Generated code - The UnitOfWork class, SelfServicing
Preface
Sometimes actions on entities span a longer timeframe and / or multiple
screens. It's then often impossible to start a database transaction as
user-interaction during a transaction should be avoided. To track all the
changes made and to persist them in one transaction can then be a tedious
task. With the
UnitOfWork class this can be solved. The
UnitOfWork
class lets you collect actions on entities or collections of entities
and can perform these actions in one atomic action. The
UnitOfWork
class is binary serializable which means it can travel across remoting
boundaries.
Entities and entity collections added to the
UnitOfWork
class are not aware that they're added to that class, so if you decide not to continue with a given
UnitOfWork instance you can simply let it get out of
scope.
UnitOfWork objects figure out the order in which actions have to be performed automatically: first Inserts, then Updates and then Deletes. This is controllable, see the section below about
Specifying the order in which the actions are executed.
UnitOfWork usage: single entities
A
UnitOfWork class can work with single entities or collections of
entities. This paragraph discusses the UnitOfWork class with single
entities. Actions collected by the
UnitOfWork class are not yet
performed, but are performed when the
UnitOfWork's
Commit()
method is called.
The
UnitOfWork class works with
Add methods for an entity and a given action:
AddForSave() or
AddForDelete(). You can specify additional parameters for
the action: recursive saves and save / delete restriction filters. A delete action for a new entity is ignored. The following example illustrates both methods.
First a recursive save is added and after that a delete action. The actions are not executed until
Commit() is called.
Commit() always expects a valid
Transaction
object which is used to run persistent actions in. You can commit more than one
UnitOfWork object in one transaction, simply pass the same
Transaction object to all
Commit() calls, passing false for autoCommit.
Commit() can also auto-commit the transaction, if all the actions succeed, you then have to use the overload of
Commit() which expects a boolean, autoCommit. You can commit more
than one
UnitOfWork object in one transaction, simply pass the same
Transaction object to all
Commit() calls, passing false for
autoCommit.
// C#
CustomerEntity newCustomer = new CustomerEntity();
// ... fill newCustomer's data
AddressEntity newAddress = new AddressEntity();
// ... fill newAddress's data
newCustomer.VisitingAddress = newAddress;
newCustomer.BillingAddress = newAddress;
UnitOfWork uow = new UnitOfWork();
// add the customer for a recursive save action.
uow.AddForSave(newCustomer, true);
ProductEntity productToDelete = new ProductEntity(productID);
// add this product for deletion.
uow.AddForDelete(newProduct);
// commit all actions in one go
uow.Commit(new Transaction(IsolationLevel.ReadCommitted, "UOW"), true);
' VB.NET
Dim newCustomer As New CustomerEntity()
' ... fill newCustomer's data
Dim newAddress As New AddressEntity()
' ... fill newAddress's data
newCustomer.VisitingAddress = newAddress
newCustomer.BillingAddress = newAddress
Dim uow As New UnitOfWork()
' add the customer for a recursive save action.
uow.AddForSave(newCustomer, True)
Dim productToDelete As New ProductEntity(productID)
' add this product for deletion.
uow.AddForDelete(newProduct)
' commit all actions in one go
uow.Commit(New Transaction(IsolationLevel.ReadCommitted, "UOW"), True)
After the
Commit() action, the database has two new entities, the customer and the address, and the product entity is deleted. These actions are taken place
inside a new transaction and when
Commit() was called, which auto-commits the transaction at the end of the actions.
UnitOfWork usage: entity collections
Sometimes a complete collection of entities has to be saved, or deleted.
Instead of adding all entities individually, you can add a collection in one
go for a given action: save or delete. The following example loads an
Order and its
OrderDetail entities and deletes the
OrderDetail
entities while updating the
Order entity. The entities in the
collection are examined when
Commit() is called. This means that an
entity which is in the collection when the collection is added to the
UnitOfWork object and is removed from that collection after that action
but before
Commit() is called, is not processed by the
UnitOfWork,
as the entity is no longer part of the collection being processed.
// C#
OrderEntity order = new OrderEntity(10254);
// load order detail entities through lazy loading
OrderDetailsCollection orderDetails = order.OrderDetails;
UnitOfWork uow = new UnitOfWork();
// alter order
order.EmployeeID = 3;
// add the order for save, no recursion.
uow.AddForSave(order);
uow.AddCollectionForDelete(orderDetails);
// commit all actions in one go
uow.Commit(new Transaction(IsolationLevel.ReadCommitted, "UOW"), true);
' VB.NET
Dim order As New OrderEntity(10254)
' load order detail entities through lazy loading
Dim orderDetails As OrderDetailsCollection = order.OrderDetails
Dim uow As New UnitOfWork()
' alter order
order.EmployeeID = 3
' add the order for save, no recursion.
uow.AddForSave(order)
uow.AddCollectionForDelete(orderDetails)
' commit all actions in one go
uow.Commit(New Transaction(IsolationLevel.ReadCommitted, "UOW"), True)
When
Commit() is called, first all entities in the collection added are added as entities for the Delete action. After that, all actions are executed, first the
save action, then the deletes.
UnitOfWork usage: stored procedures
The
UnitOfWork class is able to collect calls to stored procedures as well, and lets you schedule these calls with the work already added to the
UnitOfWork class, using four slots. The support for stored procedure calls is done through delegates. This means that you can use this feature also for your own
methods, as long as there is a delegate defined for that method. If you want to accept the actual
Transaction object, you have to make sure the method accepts a
Transaction
object as the last parameter.
Adding a stored procedure call can only be done for Action procedure calls. To add a stored procedure call, you'll use the
AddCallBack method, which accepts
a System.Delegate object, a slot enum value which schedules the call, and zero or more parameters. Below are the slot definitions listed on which you can
schedule a stored procedure call.
Enum Value |
Description |
PreEntityInsert |
Execute the callback before the first entity is inserted. |
PreEntityUpdate |
Execute the callback after the last entity has been inserted but before the first entity will be updated. |
PreEntityDelete |
Execute the callback after the last entity has been updated but before the first entity will be deleted. |
PostEntityDelete |
Execute the callback after the last entity has been deleted. |
LLBLGen Pro generates for each Action procedure call a Delegate definition. Using such a generated delegate definition, you could add a call to a
stored procedure using the following code. It adds a call to the ClearTestRunData stored procedure. It specifies that the
Transaction has to be
passed into the procedure so the call will run in the same transaction as the rest of the calls the
UnitOfWork object will make. If that's not
done, the action procedure will create it's own transaction. The call is scheduled right before the Delete calls are made on entities.
// C#
UnitOfWork uow = new UnitOfWork();
uow.AddCallBack(new ActionProcedures.ClearTestRunDataCallBack(ActionProcedures.ClearTestRunData),
UnitOfWorkCallBackScheduleSlot.PreEntityDelete, true, _testRunID);
' VB.NET
Dim uow As New UnitOfWork()
uow.AddCallBack(New ActionProcedures.ClearTestRunDataCallBack(ActionProcedures.ClearTestRunData), _
UnitOfWorkCallBackScheduleSlot.PreEntityDelete, True, _testRunID)
UnitOfWork usage: DeleteMulti and UpdateMulti
Besides adding calls to stored procedures, the
UnitOfWork object can also accept calls to
DeleteMulti and
UpdateMulti. You add calls to one of these
methods by using one of the overloads of
AddDeleteMultiCall or
AddUpdateMultiCall, by specifying the collection the call has to be made on and the
required parameters. The calls will be executed inside the active transaction used by
Commit. The
DeleteMulti call will be executed
after
the entity delete actions but
before the
PostEntityDelete callbacks. The
UpdateMulti call will be executed after the last entity has been
updated but before the
PreEntityUpdate callbacks.
Specifying the order in which the actions are executed
The
UnitOfWork class typically executes the actions added to it in the following order: CallBacks for the PreEntityInsert slot, Inserts,
CallBacks for the PreEntityUpdate slot, Updates, UpdateMulti calls, CallBacks for the PreEntityDelete slot, Deletes, CallBacks for the PostEntityDelete slot, DeleteMulti calls. This order can be too limited, for example if you first have to delete an entity before a new insert can take place because the entity to insert has the same value for a field with a unique constraint.
LLBLGen Pro lets you define entity oriented actions to be ordered in a different order, so the UnitOfWork class will for example first execute the deletes and then the updates. This is done by specifying a list of
UnitOfWorkBlockType values for the property
unitOfWork.
CommitOrder. By default you don't have to specify any commit order, the UnitOfWork class will follow the sequence as specified above. However as soon as you specify a list of UnitOfWorkBlockType values for CommitOrder, it will use that list instead. This means that if you omit a block type, these actions aren't executed at all. Duplicates are filtered out so specifying a blocktype twice has no effect, the second one is ignored.
CallBacks with the name Pre
action or Post
action belong to the blocktype of
action and will be executed in that block, in the same order as described above, so for example PreUpdateEntity callbacks are executed before the updates, when the blocktype for updates is specified to be executed.